package portfolio;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Comment;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class XMLParser implements DatabaseParser{
	
	private DocumentBuilderFactory docBuildFact;
	private DocumentBuilder docBuild;
	private Document doc;
	
	private File openFile;
	private File saveFile;
	
	public boolean verbose = false;
	
	Map<Integer,Page> allPages;
	
	public XMLParser(){
		this(	new File("resources/database.xml"), 
				new File("resources/database1.xml"),
				new HashMap<Integer, Page>()
			);
	}
	
	/**
	 * Opens, reads, and writes from the XML Document specified. 
	 * 
	 * @param file the location of the XML Document.
	 */
	public XMLParser(File file){
		this(	file,
				file,
				new HashMap<Integer, Page>()
			);
	}
	
	/**
	 * Open and reads from one XML Document and writes to another.
	 * 
	 * @param open the file location of the XML Document
	 * @param save the destination file location of the XML Document
	 */
	public XMLParser(File open, File save) {
		this(	open,
				save,
				new HashMap<Integer, Page>()
			);
	}
	
	/**
	 * Open and reads from one XML Document and writes to another.
	 * 
	 * @param openFile the file location of the XML Document
	 * @param saveFile the destination file location of the XML Document
	 * @param pages pre-fill the local database. Will be overwritten when we read the open file the first time.
	 */
	public XMLParser(File openFile, File saveFile, HashMap<Integer, Page> pages){
		this.openFile = openFile;
		this.saveFile = saveFile;
		allPages = pages;
	}

	@Override	
	public boolean connectToDatabase() {
		
		// Open and connect to the xml doc
		docBuildFact = DocumentBuilderFactory.newInstance();
		try {
			docBuild = docBuildFact.newDocumentBuilder();
			doc = docBuild.parse(openFile);			
			return true;
		} catch (ParserConfigurationException e1) {
			e1.printStackTrace();
			return false;
		} catch (SAXException e) {
			e.printStackTrace();
			return false;
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
	}
		
	/*
	 * @see portfolio.DatabaseParser#readDatabase()
	 */
	@Override
	public boolean readDatabase(){
		
		
		// Make a list of all Pages
		NodeList listOfPages = doc.getElementsByTagName("PAGE");
		
		verbose("There are " + listOfPages.getLength() + " pages in database.xml");
		
		if(listOfPages.getLength() > 0){
			verbose("The HashMap has been cleared.");
			allPages = new HashMap<Integer, Page>();
		}
		
		
		// Loops through the list of Pages and extract their data.
		// LIKE A BOSS!
		for(int i = 0; i < listOfPages.getLength(); i++){
			
			// Load all the elements into a single node
			Node page = listOfPages.item(i);
			
			// No idea why this comparison is needed
			if(page.getNodeType() == Node.ELEMENT_NODE){
				
				Element pageElement = (Element) page;

				//Pull the index
				String attribute = pageElement.getAttribute("index");
				verbose(attribute);
				Integer index = 0;
				try{
					index = Integer.parseInt(attribute);
				} catch (NumberFormatException e) {
					verbose("We have a falty index at XML Page[" + i + "]");
				}
				
				// Pull the owner
				NodeList ownerList = pageElement.getElementsByTagName("ARTIST");
				Element fOwner = (Element) ownerList.item(0);
				verbose(fOwner.getTextContent());
				String owner = fOwner.getTextContent();
				
				// Pull the type
				NodeList typeList = pageElement.getElementsByTagName("TYPE");
				Element fType = (Element) typeList.item(0);
				verbose(fType.getTextContent());
				String type = fType.getTextContent();
				
				// Pull the original
				NodeList originalList = pageElement.getElementsByTagName("ORIGINAL");
				Element fOriginal = (Element) originalList.item(0);
				verbose(fOriginal.getTextContent());
				String orginal = fOriginal.getTextContent();
				
				// Pull the thumb
				NodeList thumbList = pageElement.getElementsByTagName("THUMB");
				Element fThumb = (Element) thumbList.item(0);
				verbose(fThumb.getTextContent());
				String thumb = fThumb.getTextContent();
				
				// Pull the title
				NodeList titleList = pageElement.getElementsByTagName("TITLE");
				Element fTitle = (Element) titleList.item(0);
				verbose(fTitle.getTextContent());
				String title = fTitle.getTextContent();
				
				// Pull the date
				NodeList dateList = pageElement.getElementsByTagName("DATE");
				Element fDate = (Element) dateList.item(0);
				verbose(fDate.getTextContent());
				DateFormat df = new SimpleDateFormat("MM/dd/yyyy");
				Date date;
				try {
					date = df.parse(fDate.getTextContent());
				} catch (DOMException e) {
					e.printStackTrace();
					date = null;
				} catch (ParseException e) {
					verbose(fDate.getTextContent() + " is not a properlly formatted date.");
					date = null;
					//e.printStackTrace();
				}
				//verbose(date.toString());
				
				// Pull the medium
				NodeList mediumList = pageElement.getElementsByTagName("MEDIUM");
				Element fMedium = (Element) mediumList.item(0);
				verbose(fMedium.getTextContent());
				String medium = fMedium.getTextContent();
				
				// Pull the award
				NodeList awardList = pageElement.getElementsByTagName("AWARD");
				Element fAward = (Element) awardList.item(0);
				verbose(fAward.getTextContent());
				String award = fAward.getTextContent();
				
				// Pull the comments
				NodeList commentsList = pageElement.getElementsByTagName("COMMENTS");
				Element fComments = (Element) commentsList.item(0);
				verbose(fComments.getTextContent());
				String comments = fComments.getTextContent();
				
				Page slide = new Page(index, owner, type, orginal, thumb, title, date, medium, award, comments);
				verbose("\n" + slide.getIndex() + " is " + slide.isValid());
				verbose("--------------");
				
				if(slide.isValid())
					this.addPage(slide);
				
			} else {
				verbose("ELEMENT If statement fail");
			}
		}
		
		
		return false;
	}
	
	/* (non-Javadoc)
	 * @see portfolio.DatabaseParser#writeToDatabase(portfolio.Page)
	 */
	@Override
	public boolean writeToDatabase(Page page) {
		return printToDatabase(page, false);
	}
	
	@Override
	public boolean replacePageAt(Page page, int key) {
		if(!allPages.containsKey(key))
			return false;
		
		return printToDatabase(page, true);
	}

	@Override
	public boolean replaceDatabase(List<Page> pages) {
		
		NodeList catalog = doc.getElementsByTagName("PAGE");
		NodeList user = doc.getElementsByTagName("CATALOG");
		Node book = user.item(0);
		verbose("The database is : " + catalog.getLength() + " long!");
		verbose("But the node list is only: " + book.getChildNodes().getLength() + book.getTextContent());
		
		Element mag = (Element)book;
		
		while(mag.hasChildNodes()){
			mag.removeChild(mag.getFirstChild());
		}
		
		verbose("Now it's cleared");
		
		
		// Wipe local storage of database
		allPages = 	new HashMap<Integer, Page>();

		// Prep the iterator
		Iterator<Page> pi = pages.iterator();
		
		System.out.println();
		
		// Add each Page in the iterator to the database
		while(pi.hasNext()){
			Page sheet = pi.next();
			
			String output = String.format("%1$tm/%1$te/%1$tY", sheet.getDate());
			verbose(output);
			
			if(sheet.isValid()){
				printToDatabase(sheet,true);
			}
			
		}
		
		// For error checking. No real functional purpose. 
		if(allPages.isEmpty())
			return false;
		else
			return true;
	}

	@Override
	public boolean removePageAt(int key) {
		return false;
	}

	public Page getPage(int key){
		
		return allPages.get(key);
	}
	
	@Override
	public List<Page> getAllPages(){
		
		// Check for an empty or null HashMap
		if(allPages.isEmpty())
			return new ArrayList<Page>();
		
		// Prepare a ArrayList of Pages
		ArrayList<Page> book = new ArrayList<Page>();
		
		// Prepare the iterator made of the HashMap's keys
		Set<Integer> keys = allPages.keySet();
		Iterator<Integer> iterator = keys.iterator();
		
		// Iterate through all through the HashMap's keys and
		// add their values to the ArrayList
		while(iterator.hasNext()){
			Page sheet = allPages.get(iterator.next());
			book.add(sheet);	
		}
		
		// Return finished product
		return book;
	}
	
	@Override
	public boolean closeConnection() {
		// TODO Auto-generated method stub
		return false;
	}

	private boolean hasPage(int key){
		if(allPages.containsKey(key))
			return true;
		else
			return false;
	}

	private boolean addPage(Page newPage){
		
		// Make sure the new Page is valid (contains all required information)
		if(!newPage.isValid())
			return false;
		
		// Make sure we're not replacing a pre-existing Page
		// If we DO want to replace a page, use replacePage(Page newPage, int key)
		// THIS IS A DESIGN DECISION!
		if(this.hasPage(newPage.getIndex()))
			return false;
		
		// Adds the new Page to the HashMap
		allPages.put(newPage.getIndex(), newPage);
		
		return false;
	}
	
	private boolean removePage(int key){
		// Removes a value and then checks to see if we actually removed anything.
		return (allPages.remove(key) == null);
	}
	
	/**
	 * Print pages to the database. If <code>override</code> is true, then we are allowed to
	 * overwrite Pages that are already in the database that have the same index.
	 * 
	 * @param page the page you wish to add to the database
	 * @param override a boolean flag to override pre existing pages should they exist.
	 * @return TRUE if we're successful, FALSE if we encounter any errors
	 */
	private boolean printToDatabase(Page page, boolean override) {
		
		// Assume doc has been properly implemented
		
		// If we cannot override AND the page already exists. Stop right here!
		if(!override && allPages.containsKey(page.getIndex())){
			verbose("A Page with key(" + page.getIndex() + ") already exists");
			verbose("Page not added.");
			return false;
		}
			
		
		// Writing to the XML Database
		NodeList user = doc.getElementsByTagName("CATALOG");
		Node book = user.item(0);
		
		Element sheet = doc.createElement("PAGE");
		
		// index
		Integer iIndex = new Integer(page.getIndex());
		sheet.setAttribute("index", iIndex.toString());
		
		// owner
		Element artist = doc.createElement("ARTIST");
		artist.setTextContent(page.getOwner());
		sheet.appendChild(artist);
		
		// type
		Element type = doc.createElement("TYPE");
		type.setTextContent(page.getFileType());
		sheet.appendChild(type);
		
		// original
		Element original = doc.createElement("ORIGINAL");
		original.setTextContent(page.getOriginal());
		sheet.appendChild(original);
		
		// thumb
		Element thumb = doc.createElement("THUMB");
		thumb.setTextContent(page.getThumb());
		sheet.appendChild(thumb);
		
		// title
		Element title = doc.createElement("TITLE");
		title.setTextContent(page.getTitle());
		sheet.appendChild(title);
		
		// title
		Element date = doc.createElement("DATE");
		Date iDate = page.getDate();
		date.setTextContent(String.format("%1$tm/%1$te/%1$tY", iDate));
		sheet.appendChild(date);
		
		// medium
		Element medium = doc.createElement("MEDIUM");
		medium.setTextContent(page.getMedium());
		sheet.appendChild(medium);
		
		// awards
		Element awards = doc.createElement("AWARD");
		awards.setTextContent(page.getAwards());
		sheet.appendChild(awards);
		
		// comments
		Element comments = doc.createElement("COMMENTS");
		comments.setTextContent(page.getComments());
		sheet.appendChild(comments);
		
		
		if(override){			
			NodeList catalog = doc.getElementsByTagName("PAGE");
			
			if(catalog.getLength() > 0){
				verbose("\nWe have " + catalog.getLength() + " Pages to search.");
				boolean found = false;
				for(int i = 0; i < catalog.getLength(); i++){
					// Grab a Page
					Element pg = (Element)catalog.item(i);
					
					// Find values in attribute INDEX
					String indexAtt = pg.getAttribute("index");
					int check = Integer.parseInt(indexAtt);
					
					// Check to see if it's what we're looking for
					if(check == page.getIndex()){
						verbose("We found index: " + check + "\n----\n");
						// Replace old element with new
						book.replaceChild(sheet, pg);
						verbose(page.getIndex() + " aready exists AND was overridden with a new Page");
						found = true;
						break;
					}
						
				}
				
				if(!found)
					book.appendChild(sheet);
				
			} else {
				// Add all to the PAGE
				book.appendChild(sheet);
			}
		} else {		
			// Add all to the PAGE
			book.appendChild(sheet);
		}
		// Print		
		TransformerFactory tFactory = TransformerFactory.newInstance();
		tFactory.setAttribute("indent-number", 4);
		
		Transformer transformer = null;
		try {
			transformer = tFactory.newTransformer();
			
		    transformer.setOutputProperty(OutputKeys.METHOD, "xml");
		    transformer.setOutputProperty(OutputKeys.ENCODING,"ISO-8859-1");
		    
			transformer.setOutputProperty(OutputKeys.INDENT,"yes");			
		} catch (TransformerConfigurationException e1) {
			verbose("Unable to create a Transfermer");
			return false;
			//e1.printStackTrace();
		}
		
		DOMSource source = new DOMSource(doc);
		StreamResult result = new StreamResult(saveFile);
		
		
		try {
			// Actually print to file
			transformer.transform(source, result);
			allPages.put(page.getIndex(), page);
			return true;
		} catch (TransformerException e) {
			verbose("Unable to print to XML Doc");
			return false;
			//e.printStackTrace();
		}
		
		
		// We should never reach this comment
	}
	
	public void verbose(String message){
		if(verbose)
			System.out.println(message);
	}

	@Override
	public boolean saveDatabase() {
		Set<Integer> keys = allPages.keySet();
		Iterator<Integer> it = keys.iterator();
		
		while(it.hasNext()){
			printToDatabase(allPages.get(it.next()), true);
		}
		
		return true;
	}

}

